﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

// TODO: ByRef parameters?
// TODO: Implicit variable support

namespace ConnectBasic.Execution
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using ConnectDebug;
    using ConnectBasic.Language;
    using ConnectBasic.ObjectModel;
    using Microsoft.Scripting;
    // DLR Types, aliased to prevent conflict with our AST
    using Dlr = Microsoft.Linq.Expressions;
    using DlrExpression = Microsoft.Linq.Expressions.Expression;
    using DlrLambdaExpression = Microsoft.Linq.Expressions.LambdaExpression;
    using DlrParameterExpression = Microsoft.Linq.Expressions.ParameterExpression;

    public sealed class BasicCodeGenerator
    {
        private static readonly MethodInfo MethodInfo_SetModuleValue = typeof(BasicRuntimeHelpers).GetMethod("SetModuleValue");
        private static readonly MethodInfo MethodInfo_GetModuleValue = typeof(BasicRuntimeHelpers).GetMethod("GetModuleValue");

        private BasicLanguageContext mContext;
        private NamingScope mCurrentScope = new NamingScope();
        private DlrParameterExpression mMeParam;
        private List<KeyValuePair<BasicTokenKind, Dlr.LabelTarget>> mFlowContexts = new List<KeyValuePair<BasicTokenKind, Dlr.LabelTarget>>();
        private Dlr.SymbolDocumentInfo mSymbols;

        #region Public Static Methods

        /// <summary>
        /// Generates code for the given module.
        /// </summary>
        /// <param name="context">The language context.</param>
        /// <param name="module">The module for which to generate code.</param>
        /// <returns>A Linq expression representing the generated module.</returns>
        public static DlrExpression GenerateModule(BasicLanguageContext context, StandardModuleCodeUnit module)
        {
            if (context == null) throw new ArgumentNullException("context");
            if (module == null) throw new ArgumentNullException("module");

            return (new BasicCodeGenerator(context)).GenerateModuleCore(module);
        }

        #endregion

        #region .ctor

        private BasicCodeGenerator(BasicLanguageContext context)
        {
            mContext = context;
        }

        #endregion

        #region Private Instance Methods

        private DlrExpression GenerateModuleCore(StandardModuleCodeUnit module)
        {
            DiagnosticTreeRewriter rewriter = null;
            ProxyDebugger debugger = null;
            var methods = new List<DlrExpression>();
            var locals = new List<DlrExpression>();

            // Create a proxy for debuggers to attach to if we
            // are asked to support debugging
            if (mContext.IsDebuggingEnabled)
            {
                debugger = new ProxyDebugger();
                rewriter = new DiagnosticTreeRewriter(debugger);
                mSymbols = DlrExpression.SymbolDocument(module.Path);
            }

            foreach (var constant in module.Constants)
            {
                mCurrentScope.AddConstant(constant.ConstantName, constant.Compile(this));
            }

            foreach (var local in module.Variables)
            {
                var localType = ResolveType(local.Type);
                var localName = DlrExpression.Constant(local.Name);

                mCurrentScope.AddModuleLocal(
                    local.Name,
                    localType,
                    (me, value) => DlrExpression.Call(MethodInfo_SetModuleValue.MakeGenericMethod(localType),
                                               me,
                                               localName,
                                               value),
                    (me) => DlrExpression.Call(MethodInfo_GetModuleValue.MakeGenericMethod(localType),
                                         me,
                                         localName)
                     );

                locals.Add(DlrExpression.New(
                            typeof(LocalDefinition).GetConstructor(new Type[] { typeof(string), typeof(Type) }),
                            localName,
                            DlrExpression.Constant(localType)
                            ));
            }

            foreach (var method in module.Methods)
            {

                // Generate the method 
                var methodLambda = GenerateMethodCore(method);

                // Instrument it if we need to support debugging
                if (debugger != null)
                {                    
                    methodLambda = (Dlr.LambdaExpression)rewriter.Visit(methodLambda);
                }

                var methodBasic = DlrExpression.Call(
                    typeof(BasicRuntimeHelpers).GetMethod("MakeMethod", BindingFlags.Public | BindingFlags.Static),
                    DlrExpression.Constant(method.Name),
                    methodLambda);

                methods.Add(methodBasic);
            }

            // Give the debugger all of the debugging information we collected
            if (debugger != null)
            {
                debugger.DebugInfo = rewriter.Bake();
            }

            // Create the module
            var methodsArray = DlrExpression.NewArrayInit(typeof(BasicFunction), methods);
            var localsArray = DlrExpression.NewArrayInit(typeof(LocalDefinition), locals);

            var moduleResult = DlrExpression.Call(
                typeof(BasicRuntimeHelpers).GetMethod("MakeModule", BindingFlags.Public | BindingFlags.Static),
                DlrExpression.Constant("Unnamed Module"),
                methodsArray,
                localsArray,
                DlrExpression.Constant(debugger, typeof(ProxyDebugger)));

            return moduleResult;
        }

        private DlrLambdaExpression GenerateMethodCore(MethodDefinitionNode method)
        {

            // Figure out what our result type is
            Type resultType = ResolveType(method.ReturnType);

            // Track the flow-control scope
            var endLabel = DlrExpression.Label("Internal$End");

            if (resultType == typeof(void))
            {
                this.PushFlowContext(BasicTokenKind.KeywordSub, endLabel);
            }
            else
            {
                this.PushFlowContext(BasicTokenKind.KeywordFunction, endLabel);
            }

            // Start a new naming scope
            this.PushScope();

            // The Me/This parameter
            mMeParam = DlrExpression.Parameter(typeof(BasicModule), "#Me");
            mCurrentScope.AddMethodParameter(mMeParam);

            // Parameters
            foreach (var omParamDef in method.Parameters)
            {

                // Resolve the type of the parameter
                var paramType = ResolveType(omParamDef.Type);

                if (omParamDef.IsByRef)
                {
                    paramType = paramType.MakeByRefType();
                }

                // Create the DLR parameter
                var scriptExpParamDef = DlrExpression.Parameter(paramType, omParamDef.Name);
                mCurrentScope.AddMethodParameter(scriptExpParamDef);
            }

            // Add a variable used to hold the result
            DlrParameterExpression resultVar = null;

            if (resultType != typeof(void))
            {
                resultVar = DlrExpression.Variable(resultType, method.Name);
                mCurrentScope.AddMethodLocal(resultVar);
            }

            // Compile statements
            var statements = new List<DlrExpression>()
            {

                // The method body
                method.Statements.Compile(this),

                // The termination label
                DlrExpression.Label(endLabel)
            };

            // Need to create the body differently depending on whether
            // we're dealing with a function or a statement
            DlrExpression body;

            if (method.ReturnType.IsVoid)
            {

                // Combine the statements into a body which returns a void
                body = DlrExpression.Block(typeof(void), mCurrentScope.GetVariables(), statements);
            }
            else
            {
                // Finish the statements by returning the value of the result variable
                statements.Add(resultVar);

                // Combine the statements into a body which returns a value
                body = DlrExpression.Block(resultVar.Type, mCurrentScope.GetVariables(), statements);
            }

            // Finish the lambda
            var final = DlrExpression.Lambda(body, method.Name, mCurrentScope.GetParameters());

            // Clear state
            mMeParam = null;
            this.PopScope();
            this.PopFlowContext();

            // Return the expression
            return final;
        }

        #endregion

        #region Public Properties

        public DlrParameterExpression MeParameter
        {
            get { return mMeParam; }
        }

        public NamingScope Scope
        {
            get { return mCurrentScope; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Resolves the specified unbound type.
        /// </summary>
        /// <param name="type">The type for which to resolve a CLR type.</param>
        /// <returns></returns>
        public Type ResolveType(UnboundType type)
        {
            Type basicType;

            // Resolve the named type
            // TODO: Currency
            // TODO: Variant - properly
            switch (type.Name.ToLowerInvariant())
            {
                case "short":
                    basicType = typeof(sbyte);
                    break;
                case "integer":
                    basicType = typeof(Int16);
                    break;
                case "long":
                    basicType = typeof(Int32);
                    break;
                case "string":
                    basicType = typeof(string);
                    break;
                case "boolean":
                    basicType = typeof(Boolean);
                    break;
                case "single":
                    basicType = typeof(float);
                    break;
                case "double":
                    basicType = typeof(double);
                    break;
                case "variant":
                    basicType = typeof(object);
                    break;
                case "object":
                    basicType = typeof(object);
                    break;
                case "void": // For method returns
                    basicType = typeof(void);
                    break;
                default:
                    throw new ArgumentOutOfRangeException("type");
            }

            // See if we need to make it into an array
            switch (type.ArrayDimensions)
            {
                case 0:
                    return basicType;
                case 1:
                    return basicType.MakeArrayType();
                default:
                    return basicType.MakeArrayType(type.ArrayDimensions);
            }
        }

        public DlrExpression MakeGetNameValueExpression(string name)
        {

            // Look for a global
            SymbolId nameId = SymbolTable.StringToCaseInsensitiveId(name);
            object globalResult;

            if (mContext.DomainManager.Globals.TryGetName(nameId, out globalResult))
            {
                return DlrExpression.Constant(globalResult);
            }

            // Try the naming scope to start with 
            DlrExpression result = mCurrentScope.MakeGetValueExpression(name, mMeParam);

            // Try a constant/variable/parameter
            if (result != null)
            {
                return result;
            }

            // Look for a built in enumeration
            var enums = from t in typeof(BasicCodeGenerator).Assembly.GetTypes()
                        where t.IsEnum && t.IsDefined(typeof(BuiltinEnumerationAttribute), false)
                        select t;

            foreach (var enumType in enums)
            {
                foreach (var facet in Enum.GetNames(enumType))
                {
                    if (string.Equals(facet, name, StringComparison.OrdinalIgnoreCase))
                    {
                        return DlrExpression.Constant(Enum.Parse(enumType, name, true));
                    }
                }
            }

            // If we get here, we didn't find it
            throw new ArgumentOutOfRangeException("name");
        }

        public DlrExpression AddDebugInfo(DlrExpression expression, SourceSpan span)
        {
            if (span == SourceSpan.Invalid || span == SourceSpan.None || mSymbols == null)
            {
                return expression;
            }

            return Microsoft.Scripting.Ast.Utils.AddDebugInfo(
                        expression,
                        mSymbols,
                        span.Start,
                        span.End);
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Pushes a control-flow context which can be exited from using an Exit For/Do/While/etc
        /// statement.
        /// </summary>
        /// <param name="kind">The token kind which identifies the keyword following the Exit keyword, e.g. <see cref="BasicTokenKind.KeywordSub" />.</param>
        /// <param name="exitTarget">The label which marks the end of the control-flow construct.</param>
        internal void PushFlowContext(BasicTokenKind kind, Dlr.LabelTarget exitTarget)
        {
            mFlowContexts.Add(new KeyValuePair<BasicTokenKind, Microsoft.Linq.Expressions.LabelTarget>(kind, exitTarget));
        }

        /// <summary>
        /// Pops the current control-flow context.
        /// </summary>
        internal void PopFlowContext()
        {
            mFlowContexts.RemoveAt(mFlowContexts.Count - 1);
        }

        /// <summary>
        /// Gets the exit label for the first matching control-flow context.
        /// </summary>
        /// <param name="kind">The token kind which identifies the keyword following the Exit keyword, e.g. <see cref="BasicTokenKind.KeywordSub" />.</param>
        /// <returns>The label which marks the end of the control-flow construct, or a null reference.</returns>
        internal Dlr.LabelTarget GetFlowExitLabel(BasicTokenKind kind)
        {
            for (int i = mFlowContexts.Count - 1; i >= 0; i--)
            {
                if (mFlowContexts[i].Key == kind)
                {
                    return mFlowContexts[i].Value;
                }
            }

            return null;
        }

        #endregion

        #region Scope Handling

        private void PushScope()
        {
            mCurrentScope = new NamingScope(mCurrentScope);
        }

        private void PopScope()
        {
            mCurrentScope = mCurrentScope.Parent;
        }

        public class NamingScope
        {
            private class NameGetterAndSetter
            {
                public readonly Func<DlrParameterExpression, DlrExpression, DlrExpression> MakeSetter;
                public readonly Func<DlrParameterExpression, DlrExpression> MakeGetter;
                public readonly Type Type;

                public NameGetterAndSetter(Func<DlrParameterExpression, DlrExpression, DlrExpression> makeSetter, Func<DlrParameterExpression, DlrExpression> makeGetter, Type type)
                {
                    this.MakeSetter = makeSetter;
                    this.MakeGetter = makeGetter;
                    this.Type = type;
                }
            }

            private NamingScope mParent;
            private Dictionary<string, NameGetterAndSetter> mNames = new Dictionary<string, NameGetterAndSetter>(StringComparer.OrdinalIgnoreCase);
            private List<DlrParameterExpression> mVariables = new List<Microsoft.Linq.Expressions.ParameterExpression>();
            private List<DlrParameterExpression> mParameters = new List<Microsoft.Linq.Expressions.ParameterExpression>();

            public NamingScope()
            {

            }

            public NamingScope(NamingScope parent)
            {
                mParent = parent;
            }

            public NamingScope Parent
            {
                get { return mParent; }
            }

            /// <summary>
            /// Creates an expression which will get the value of the member with the given name.
            /// </summary>
            /// <param name="name"></param>
            /// <returns></returns>
            public DlrExpression MakeGetValueExpression(string name, DlrParameterExpression meParam)
            {
                NameGetterAndSetter result;

                if (mNames.TryGetValue(name, out result))
                {
                    return result.MakeGetter(meParam);
                }
                else if (mParent != null)
                {
                    return mParent.MakeGetValueExpression(name, meParam);
                }
                else
                {
                    return null;
                }
            }

            /// <summary>
            /// Creates an expression which will set the value of the member with the given name.
            /// </summary>
            /// <param name="name"></param>
            /// <param name="value"></param>
            /// <returns></returns>
            public DlrExpression MakeSetValueExpression(string name, DlrParameterExpression meParam, Func<Type, DlrExpression> makeAssignmentInner)
            {
                NameGetterAndSetter result;

                if (mNames.TryGetValue(name, out result))
                {
                    return result.MakeSetter(meParam, makeAssignmentInner(result.Type));
                }
                else if (mParent != null)
                {
                    return mParent.MakeSetValueExpression(name, meParam, makeAssignmentInner);
                }
                else
                {
                    return null;
                }
            }

            /// <summary>
            /// Adds a constant to the current lexical scope.
            /// </summary>
            /// <param name="name"></param>
            /// <param name="value"></param>
            public void AddConstant(string name, DlrExpression value)
            {
                this.AddNameCore(name,
                                 value.Type,
                                 null,
                                 (me) => value);
            }

            /// <summary>
            /// Adds a parameter to the current lexical scope (only for functions).
            /// </summary>
            /// <param name="value"></param>
            public void AddMethodParameter(DlrParameterExpression parameter)
            {
                this.AddNameCore(parameter.Name,
                                 parameter.Type,
                                 (me, valueIn) => DlrExpression.Assign(parameter, valueIn),
                                 (me) => parameter);

                mParameters.Add(parameter);
            }

            /// <summary>
            /// Adds a local variable to the current lexical scope (only for functions).
            /// </summary>
            /// <param name="value"></param>
            public void AddMethodLocal(DlrParameterExpression local)
            {
                this.AddNameCore(local.Name,
                                 local.Type,
                                 (me, valueIn) => DlrExpression.Assign(local, valueIn),
                                 (me) => local);

                mVariables.Add(local);
            }

            /// <summary>
            /// Adds a module-level local variable.
            /// </summary>
            /// <param name="name"></param>
            /// <param name="type"></param>
            /// <param name="makeSetter"></param>
            /// <param name="makeGetter"></param>
            public void AddModuleLocal(string name, Type type, Func<DlrParameterExpression, DlrExpression, DlrExpression> makeSetter, Func<DlrParameterExpression, DlrExpression> makeGetter)
            {
                if (mParent != null) throw new InvalidOperationException("Module scopes can't have parents!");
                if (type == null) throw new ArgumentNullException("type");
                if (makeSetter == null) throw new ArgumentNullException("makeSetter");
                if (makeGetter == null) throw new ArgumentNullException("makeGetter");

                this.AddNameCore(name, type, makeSetter, makeGetter);
            }

            public IEnumerable<DlrParameterExpression> GetVariables()
            {
                return mVariables;
            }

            public IEnumerable<DlrParameterExpression> GetParameters()
            {
                return mParameters;
            }

            private void AddNameCore(string name, Type type, Func<DlrParameterExpression, DlrExpression, DlrExpression> makeSetter, Func<DlrParameterExpression, DlrExpression> makeGetter)
            {
                mNames.Add(name, new NameGetterAndSetter(makeSetter, makeGetter, type));
            }
        }

        #endregion

    }
}
